﻿using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using log4net;

namespace MyLib {
	/// <summary>
	/// Extensions for XML
	/// </summary>
	public static class XmlExtensions {
		/// <summary>
		/// Internal log.
		/// </summary>
		private static ILog m_logger = LogManager.GetLogger(typeof(XmlExtensions));

		/// <summary>
		/// Load instance of XElement.
		/// </summary>
		/// <param name="stream"></param>
		/// <param name="schemas"></param>
		/// <returns></returns>
		public static XElement LoadXElement(Stream stream, System.Xml.Schema.XmlSchemaSet schemas = null) {
			try {
				var doc = XDocument.Load(stream, LoadOptions.SetLineInfo);

				if (schemas != null) {
					doc.Validate(schemas, ValidationCallback, true);
				}

				return doc.Root;
			} catch (System.Xml.Schema.XmlSchemaException se) {
				var fn = "?";
				if (stream is FileStream)
					fn = (stream as FileStream).Name;
				var psi = string.Format("Validating schema failed at {0}({1}:{2})", fn, se.LineNumber, se.LinePosition);
				throw new ParseXmlException(psi, null, se);
			} catch (System.Xml.XmlException xe) {
				var fn = "?";
				if (stream is FileStream)
					fn = (stream as FileStream).Name;
				var psi = string.Format("Invalid xml content at {0}({1}:{2})", fn, xe.LineNumber, xe.LinePosition);
				throw new ParseXmlException(psi, null, xe);
			} catch (System.Exception ex) {
				throw new ParseXmlException(ex.Message, null, ex);
			}
		} // end of LoadXElement.

		/// <summary>
		/// Load instance of XElement.
		/// </summary>
		/// <param name="reader"></param>
		/// <param name="schemas"></param>
		/// <returns></returns>
		public static XElement LoadXElement(TextReader reader, System.Xml.Schema.XmlSchemaSet schemas = null) {
			try {
				var doc = XDocument.Load(reader, LoadOptions.SetLineInfo);

				if (schemas != null) {
					doc.Validate(schemas, ValidationCallback, true);
				}

				return doc.Root;
			} catch (System.Xml.Schema.XmlSchemaException se) {
				var psi = string.Format("Validating schema failed at {0}({1}:{2})", "?", se.LineNumber, se.LinePosition);
				throw new ParseXmlException(psi, null, se);
			} catch (System.Xml.XmlException xe) {
				var psi = string.Format("Invalid xml content at {0}({1}:{2})", "?", xe.LineNumber, xe.LinePosition);
				throw new ParseXmlException(psi, null, xe);
			} catch (System.Exception ex) {
				throw new ParseXmlException(ex.Message, null, ex);
			}
		} // end of LoadXElement.

		/// <summary>
		/// 
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private static void ValidationCallback(object sender, System.Xml.Schema.ValidationEventArgs e) {
			if (e.Exception != null)
				throw e.Exception;
			else
				throw new System.Xml.Schema.XmlSchemaException(e.Message);
		}

		/// <summary>
		/// Find XElement with special name.
		/// </summary>
		/// <param name="elements"></param>
		/// <param name="nameExpected"></param>
		/// <param name="ignoreCase"></param>
		/// <returns></returns>
		public static IEnumerable<XElement> NameEquals(this IEnumerable<XElement> elements, string nameExpected, bool ignoreCase = true) {
			return elements.Where(_ => string.Compare(_.Name.LocalName, nameExpected, ignoreCase) == 0);
		}

		/// <summary>
		/// Find XElement with special attribute.
		/// </summary>
		/// <param name="elements"></param>
		/// <param name="attributeName"></param>
		/// <param name="attributeExpected"></param>
		/// <param name="ignoreCase"></param>
		/// <returns></returns>
		public static IEnumerable<XElement> AttributeEquals(this IEnumerable<XElement> elements, string attributeName, string attributeExpected, bool ignoreCase = true) {
			return elements.Where(_ => {
				var attr = _.Attribute(attributeName);
				if (attr == null) return false;

				return string.Compare(attr.Value, attributeExpected, ignoreCase) == 0;
			});
		}

		/// <summary>
		/// Find property of element.
		/// </summary>
		/// <param name="element"></param>
		/// <param name="attributeName"></param>
		/// <returns></returns>
		public static string Property(this XElement element, string attributeName) {
			var attr = element.Attribute(attributeName);
			if (attr != null) {
				return attr.Value;
			}

			var attrEle = element.Elements(attributeName).FirstOrDefault();
			if (attrEle != null)
				return attrEle.Value;
			else
				return string.Empty;
		}

		/// <summary>
		/// Find properties of element.
		/// </summary>
		/// <param name="element"></param>
		/// <param name="attributeName"></param>
		/// <returns></returns>
		public static string[] Properties(this XElement element, string attributeName) {
			var attr = element.Attribute(attributeName);
			if (attr != null) {
				return attr.Value.Split(',', ';');
			}

			var attrEle = element.Elements(attributeName);
			if (attrEle != null)
				return attrEle.Select(_ => _.Value).ToArray();
			else
				return new string[] { };
		}

		/// <summary>
		/// Get value of attribute.
		/// </summary>
		/// <param name="xa"></param>
		/// <returns></returns>
		public static string StringValue(this XAttribute xa) {
			if (xa == null)
				return "";
			else
				return xa.Value;
		}

		/// <summary>
		/// Get value of attribute.
		/// </summary>
		/// <param name="xa"></param>
		/// <returns></returns>
		public static string StringValue(this XmlAttribute xa) {
			if (xa == null)
				return "";
			else
				return xa.Value;
		}

		/// <summary>
		/// Blank line info.
		/// </summary>
		private const string EMPTY_XML_LINE_INFO = "(?,?)";

		/// <summary>
		/// Format of line info.
		/// </summary>
		private const string XML_LINE_INFO_FORMAT = "({0},{1})";

		/// <summary>
		/// Get line info of element.
		/// </summary>
		/// <param name="xo"></param>
		/// <returns></returns>
		public static string FormateLineInfo(this XObject xo) {
			if (xo == null)
				return EMPTY_XML_LINE_INFO;

			var xli = xo as IXmlLineInfo;
			if (xli == null)
				return EMPTY_XML_LINE_INFO;

			if (!xli.HasLineInfo())
				return EMPTY_XML_LINE_INFO;

			return string.Format(
				XML_LINE_INFO_FORMAT,
				xli.LineNumber,
				xli.LinePosition);
		}
	} // end of XmlExtensions.
}
